"""
中标候选人公示处理脚本

功能：
- 输入 SubDetailItem → 调用通义千问 → 生成结构化 JSON → 校验并入库 BidCandidate
"""

from typing import Dict, Iterable, List, Optional
from spider.models import SubDetailItem
from process.ai.unified import chat_with_usage
from deal.models import BidCandidate, BidEvaluationResult, Project, Company
from django.utils import timezone
from decimal import Decimal
import json
import re
from datetime import datetime


DEFAULT_SYSTEM_MSG = (
    "你是一名中标候选人公示的结构化抽取专家。"
    "请严格按输出格式返回 JSON，字段不确定时用 null 或空字符串。"
)

DEFAULT_INSTRUCTION = (
    "从页面 HTML 与上下文中识别中标候选人公示的关键信息并结构化抽取。"
    "包含候选人排名、公司名称、投标报价等；注意金额与日期的规范化；缺失字段不要猜测。"
)

DEFAULT_OUTPUT_FORMAT = (
    "输出 JSON 字段：\n"
    "announcement_date：公示日期（YYYY-MM-DD 或 ISO8601，无法确定则 null）；\n"
    "project_code：项目编号（字符串，用于入库关联，无法确定则 null）；\n"
    "original_url：原始URL；\n"
    "candidates：数组，每项为对象，字段：\n"
    "- candidate_rank：候选人排名（数字，从1开始）；\n"
    "- company_name：公司名称（字符串）；\n"
    "- bid_price：投标报价（单位元，Decimal 语义，无法确定则 null）；\n"
    "- candidate_info：对象（JSON，补充字段与说明，可为空）。"
)


def _parse_json(text: str) -> Dict:
    if not text:
        return {}
    s = text.strip()
    if s.startswith("```"):
        s = s.split("\n", 1)[1] if "\n" in s else s
        if s.endswith("```"):
            s = s[:-3].strip()
    s = re.sub(r"^\s*<think>[\s\S]*?</think>\s*", "", s)
    m = re.search(r"\{[\s\S]*\}", s)
    if m:
        frag = m.group(0)
        try:
            return json.loads(frag)
        except Exception:
            pass
    try:
        return json.loads(s)
    except Exception:
        return {}


def _parse_datetime(value: str | None) -> Optional[datetime]:
    if not value:
        return None
    value = value.strip()
    for fmt in ("%Y-%m-%d", "%Y-%m-%d %H:%M:%S", "%Y/%m/%d", "%Y/%m/%d %H:%M:%S"):
        try:
            dt = datetime.strptime(value, fmt)
            return timezone.make_aware(dt) if timezone.is_naive(dt) else dt
        except Exception:
            continue
    try:
        dt = datetime.fromisoformat(value)
        return timezone.make_aware(dt) if timezone.is_naive(dt) else dt
    except Exception:
        return None


def _parse_amount(value) -> Optional[Decimal]:
    if value is None:
        return None
    try:
        if isinstance(value, (int, float, Decimal)):
            return Decimal(str(value))
        s = str(value).strip().replace(",", "")
        return Decimal(s)
    except Exception:
        return None


def build_messages(
    item: SubDetailItem,
    instruction: Optional[str] = None,
    output_format: Optional[str] = None,
    system: Optional[str] = None,
    extra_context: Optional[Dict[str, str]] = None,
    json_payload: Optional[Dict] = None,
) -> List[Dict[str, str]]:
    sys_msg = system or DEFAULT_SYSTEM_MSG
    inst = instruction or DEFAULT_INSTRUCTION
    fmt = output_format or DEFAULT_OUTPUT_FORMAT
    ctx = extra_context or {}
    payload = json_payload if json_payload is not None else {
        "url": item.url,
        "html_content": item.html_content,
    }
    content = (
        "Instruction:\n" + inst + "\n" +
        "OutputFormat:\n" + fmt + "\n" +
        "Context:\n" + json.dumps(ctx, ensure_ascii=False) + "\n" +
        "URL:\n" + item.url + "\n" +
        "JSON:\n" + json.dumps(payload, ensure_ascii=False)
    )
    return [
        {"role": "system", "content": sys_msg},
        {"role": "user", "content": content},
    ]


def process_bid_candidate(
    item: SubDetailItem,
    instruction: Optional[str] = None,
    output_format: Optional[str] = None,
    system: Optional[str] = None,
    extra_context: Optional[Dict[str, str]] = None,
    model: str = "qwen-plus",
    stream: bool = False,
    json_payload: Optional[Dict] = None,
    **kwargs,
) -> str | Iterable[str]:
    messages = build_messages(item, instruction, output_format, system, extra_context, json_payload=json_payload)
    return chat_with_usage(messages=messages, model=model, **kwargs).get("content")


def process_and_ingest_bid_candidate(
    item: SubDetailItem,
    instruction: Optional[str] = None,
    output_format: Optional[str] = None,
    system: Optional[str] = None,
    extra_context: Optional[Dict[str, str]] = None,
    model: str = "qwen-plus",
    json_payload: Optional[Dict] = None,
    **kwargs,
) -> Dict[str, object]:
    """调用模型→校验→入库 BidCandidate（批量 upsert）。"""
    r = chat_with_usage(
        messages=build_messages(
            item=item,
            instruction=instruction,
            output_format=output_format,
            system=system,
            extra_context=extra_context,
            json_payload=json_payload,
        ),
        model=model,
        **kwargs,
    )
    payload = _parse_json(r.get("content") or "")
    errors: List[str] = []
    if not payload:
        return {"ok": False, "errors": ["结构化输出不是合法 JSON"]}

    ann_date = _parse_datetime(payload.get("announcement_date"))
    if not ann_date:
        errors.append("公示日期解析失败：announcement_date")

    original_url = payload.get("original_url") or item.url
    project_code = payload.get("project_code")
    if not project_code:
        errors.append("项目编号缺失：project_code")
        return {"ok": False, "errors": errors}

    project = Project.objects.filter(project_code=project_code).first()
    if not project:
        project = Project.objects.create(
            project_code=project_code,
            project_name=payload.get("project_name") or "未命名项目",
            owner_name="未知业主",
            proxy_company="",
            project_type="",
            service_period="",
            budget_amount=None,
            extra_data={},
            crawl_task_id=item.task_id,
        )

    # 定位或创建评标结果
    eval_result = BidEvaluationResult.objects.filter(announcement_date=ann_date, project=project).first()
    if not eval_result:
        eval_result = BidEvaluationResult.objects.create(
            announcement_date=ann_date,
            original_url=original_url,
            project=project,
            opening_record=None,
            crawl_task_id=item.task_id,
        )

    candidates = payload.get("candidates") or []
    if not isinstance(candidates, list) or not candidates:
        errors.append("候选人列表为空：candidates")
        return {"ok": False, "errors": errors}

    created = 0
    updated = 0
    company_missing = False
    amount_missing = False

    for c in candidates:
        try:
            rank = c.get("candidate_rank")
            company_name = c.get("company_name")
            bid_price = _parse_amount(c.get("bid_price"))
            info = c.get("candidate_info") or {}
            if not isinstance(info, dict):
                info = {"raw": info}

            if rank is None:
                errors.append("候选人缺少排名：candidate_rank")
                continue
            if not company_name:
                company_missing = True
                errors.append(f"候选人缺少公司名称（rank={rank}）：company_name")
                continue

            company = Company.objects.filter(company_name=company_name).first()
            if not company:
                from uuid import uuid4
                company = Company.objects.create(
                    company_name=company_name,
                    unified_social_credit_code=f"AUTO-{uuid4().hex[:12]}",
                    company_info={},
                    qualification_info={},
                )

            defaults = {
                "bid_price": (bid_price or Decimal("0")),
                "company": company,
                "candidate_info": info,
                "crawl_task_id": item.task_id,
            }
            obj, was_created = BidCandidate.objects.get_or_create(
                candidate_rank=int(rank),
                bid_evaluation_result=eval_result,
                defaults=defaults,
            )
            if was_created:
                created += 1
            else:
                # 更新现有记录
                obj.bid_price = defaults["bid_price"]
                obj.company = defaults["company"]
                obj.candidate_info = defaults["candidate_info"]
                obj.crawl_task_id = defaults["crawl_task_id"]
                obj.save()
                updated += 1

            if bid_price is None or bid_price <= 0:
                amount_missing = True

        except Exception as e:
            errors.append(f"候选人处理异常：{e!r}")

    # 标记任务完成
    if item.task_id and item.task:
        item.task.status = "crawled"
        item.task.crawled_at = timezone.now()
        item.task.save(update_fields=["status", "crawled_at"]) 

    # 质量标记：公司或金额缺失
    if company_missing or amount_missing:
        item.is_suspect = True
        item.save(update_fields=["is_suspect"]) 

    if created == 0 and updated == 0:
        return {"ok": False, "errors": errors or ["未能创建或更新任何候选人"]}

    return {
        "ok": True,
        "id": None,
        "action": "upsert",
        "created": created,
        "updated": updated,
        "errors": errors,
        "summary": {
            "url": original_url,
            "project_name": project.project_name,
            "units": [c.get("company_name") for c in (payload.get("candidates") or []) if isinstance(c, dict) and c.get("company_name")],
            "amount": None,
        },
        "usage": r.get("usage") or {},
    }


__all__ = [
    "build_messages",
    "process_bid_candidate",
    "process_and_ingest_bid_candidate",
]